// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE

;(function (mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('codemirror'), require('codemirror/mode/sql/sql'))
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['codemirror', 'codemirror/mode/sql/sql'], mod)
  // Plain browser env
  else mod(CodeMirror)
})(function (CodeMirror) {
  'use strict'
  var tables
  var defaultTable
  var keywords
  var identifierQuote
  var CONS = {
    QUERY_DIV: ';',
    ALIAS_KEYWORD: 'AS',
  }
  var Pos = CodeMirror.Pos,
    cmpPos = CodeMirror.cmpPos

  function isArray(val) {
    return Object.prototype.toString.call(val) == '[object Array]'
  }

  function getKeywords(editor) {
    var mode = editor.doc.modeOption
    if (mode === 'sql') mode = 'text/x-sql'
    return CodeMirror.resolveMode(mode).keywords
  }

  function getIdentifierQuote(editor) {
    var mode = editor.doc.modeOption
    if (mode === 'sql') mode = 'text/x-sql'
    return CodeMirror.resolveMode(mode).identifierQuote || '`'
  }

  function getText(item) {
    return typeof item == 'string' ? item : item.text
  }

  function wrapTable(name, value) {
    if (isArray(value)) value = { columns: value }
    if (!value.text) value.text = name
    return value
  }

  function parseTables(input) {
    var result = {}
    if (isArray(input)) {
      for (var i = input.length - 1; i >= 0; i--) {
        var item = input[i]
        result[getText(item).toUpperCase()] = wrapTable(getText(item), item)
      }
    } else if (input) {
      for (var name in input) result[name.toUpperCase()] = wrapTable(name, input[name])
    }
    return result
  }

  function getTable(name) {
    return tables[name.toUpperCase()]
  }

  function shallowClone(object) {
    var result = {}
    for (var key in object) if (object.hasOwnProperty(key)) result[key] = object[key]
    return result
  }

  function match(string, word) {
    var len = string.length
    var sub = getText(word).substr(0, len)
    return string.toUpperCase() === sub.toUpperCase()
  }

  function addMatches(result, search, wordlist, formatter) {
    if (isArray(wordlist)) {
      for (var i = 0; i < wordlist.length; i++)
        if (match(search, wordlist[i])) result.push(formatter(wordlist[i]))
    } else {
      for (var word in wordlist)
        if (wordlist.hasOwnProperty(word)) {
          var val = wordlist[word]
          if (!val || val === true) val = word
          else
            val = val.displayText
              ? { text: val.text, displayText: val.displayText }
              : val.text
          if (match(search, val)) result.push(formatter(val))
        }
    }
  }

  function cleanName(name) {
    // Get rid name from identifierQuote and preceding dot(.)
    if (name.charAt(0) == '.') {
      name = name.substr(1)
    }
    // replace duplicated identifierQuotes with single identifierQuotes
    // and remove single identifierQuotes
    var nameParts = name.split(identifierQuote + identifierQuote)
    for (var i = 0; i < nameParts.length; i++)
      nameParts[i] = nameParts[i].replace(new RegExp(identifierQuote, 'g'), '')
    return nameParts.join(identifierQuote)
  }

  function insertIdentifierQuotes(name) {
    var nameParts = getText(name).split('.')
    for (var i = 0; i < nameParts.length; i++)
      nameParts[i] =
        identifierQuote +
        // duplicate identifierQuotes
        nameParts[i].replace(
          new RegExp(identifierQuote, 'g'),
          identifierQuote + identifierQuote,
        ) +
        identifierQuote
    var escaped = nameParts.join('.')
    if (typeof name == 'string') return escaped
    name = shallowClone(name)
    name.text = escaped
    return name
  }

  function nameCompletion(cur, token, result, editor) {
    // Try to complete table, column names and return start position of completion
    var useIdentifierQuotes = false
    var nameParts = []
    var start = token.start
    var cont = true
    while (cont) {
      cont = token.string.charAt(0) == '.'
      useIdentifierQuotes =
        useIdentifierQuotes || token.string.charAt(0) == identifierQuote

      start = token.start
      nameParts.unshift(cleanName(token.string))

      token = editor.getTokenAt(Pos(cur.line, token.start))
      if (token.string == '.') {
        cont = true
        token = editor.getTokenAt(Pos(cur.line, token.start))
      }
    }

    // Try to complete table names
    var string = nameParts.join('.')
    addMatches(result, string, tables, function (w) {
      return useIdentifierQuotes ? insertIdentifierQuotes(w) : w
    })

    // Try to complete columns from defaultTable
    addMatches(result, string, defaultTable, function (w) {
      return useIdentifierQuotes ? insertIdentifierQuotes(w) : w
    })

    // Try to complete columns
    string = nameParts.pop()
    var table = nameParts.join('.')

    var alias = false
    var aliasTable = table
    // Check if table is available. If not, find table by Alias
    if (!getTable(table)) {
      var oldTable = table
      table = findTableByAlias(table, editor)
      if (table !== oldTable) alias = true
    }

    var columns = getTable(table)
    if (columns && columns.columns) columns = columns.columns

    if (columns) {
      addMatches(result, string, columns, function (w) {
        var tableInsert = table
        if (alias == true) tableInsert = aliasTable
        if (typeof w == 'string') {
          w = tableInsert + '.' + w
        } else {
          w = shallowClone(w)
          w.text = tableInsert + '.' + w.text
        }
        return useIdentifierQuotes ? insertIdentifierQuotes(w) : w
      })
    }

    return start
  }

  function eachWord(lineText, f) {
    var words = lineText.split(/\s+/)
    for (var i = 0; i < words.length; i++) if (words[i]) f(words[i].replace(/[`,;]/g, ''))
  }

  function findTableByAlias(alias, editor) {
    var doc = editor.doc
    var fullQuery = doc.getValue()
    var aliasUpperCase = alias.toUpperCase()
    var previousWord = ''
    var table = ''
    var separator = []
    var validRange = {
      start: Pos(0, 0),
      end: Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).length),
    }

    //add separator
    var indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV)
    while (indexOfSeparator != -1) {
      separator.push(doc.posFromIndex(indexOfSeparator))
      indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV, indexOfSeparator + 1)
    }
    separator.unshift(Pos(0, 0))
    separator.push(
      Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).text.length),
    )

    //find valid range
    var prevItem = null
    var current = editor.getCursor()
    for (var i = 0; i < separator.length; i++) {
      if (
        (prevItem == null || cmpPos(current, prevItem) > 0) &&
        cmpPos(current, separator[i]) <= 0
      ) {
        validRange = { start: prevItem, end: separator[i] }
        break
      }
      prevItem = separator[i]
    }

    if (validRange.start) {
      var query = doc.getRange(validRange.start, validRange.end, false)

      for (var i = 0; i < query.length; i++) {
        var lineText = query[i]
        eachWord(lineText, function (word) {
          var wordUpperCase = word.toUpperCase()
          if (wordUpperCase === aliasUpperCase && getTable(previousWord))
            table = previousWord
          if (wordUpperCase !== CONS.ALIAS_KEYWORD) previousWord = word
        })
        if (table) break
      }
    }
    return table
  }

  CodeMirror.registerHelper('hint', 'sql', function (editor, options) {
    // debugger
    tables = parseTables(options && options.table)
    var defaultTableName = options && options.defaultTable
    var disableKeywords = options && options.disableKeywords
    defaultTable = defaultTableName && getTable(defaultTableName)
    keywords = getKeywords(editor)
    identifierQuote = getIdentifierQuote(editor)

    if (defaultTableName && !defaultTable)
      defaultTable = findTableByAlias(defaultTableName, editor)

    defaultTable = defaultTable || []

    if (defaultTable.columns) defaultTable = defaultTable.columns

    var cur = editor.getCursor()
    var result = []
    var token = editor.getTokenAt(cur),
      start,
      end,
      search
    if (token.end > cur.ch) {
      token.end = cur.ch
      token.string = token.string.slice(0, cur.ch - token.start)
    }

    if (token.string.match(/^[.`"'\w@][\w$#]*$/g)) {
      search = token.string
      start = token.start
      end = token.end
    } else {
      start = end = cur.ch
      search = ''
    }
    if (search.charAt(0) == '.' || search.charAt(0) == identifierQuote) {
      start = nameCompletion(cur, token, result, editor)
    } else {
      var objectOrClass = function (w, className) {
        if (typeof w === 'object') {
          w.className = className
        } else {
          w = { text: w, className: className }
        }
        return w
      }
      addMatches(result, search, defaultTable, function (w) {
        return objectOrClass(w, 'CodeMirror-hint-table CodeMirror-hint-default-table')
      })
      addMatches(result, search, tables, function (w) {
        return objectOrClass(w, 'CodeMirror-hint-table')
      })
      if (!disableKeywords)
        addMatches(result, search, keywords, function (w) {
          return objectOrClass(w.toUpperCase(), 'CodeMirror-hint-keyword')
        })
    }

    return { list: result, from: Pos(cur.line, start), to: Pos(cur.line, end) }
  })
})
